// ----------------------------------------------------------------------------
// Copyright 2006, Martin D. Flynn
// All rights reserved
// ----------------------------------------------------------------------------
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// 
// http://www.apache.org/licenses/LICENSE-2.0
// 
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// ----------------------------------------------------------------------------
// Description:
//  This class maintains the structure of 'custom event packets'.  The client
//  can define it's own custom event packet structure using this class, and 
//  send this template to the server so that the server can understand how to
//  parse these custome event packets.
// ----------------------------------------------------------------------------
// Change History:
//  2006/03/26  Martin D. Flynn
//      Initial release
// ----------------------------------------------------------------------------
package org.opendmtp.j2me.client.base;

import org.opendmtp.j2me.util.GeoPoint;
import org.opendmtp.j2me.util.Payload;
import org.opendmtp.j2me.util.StringTools;

/**
 * A template for encoding and decoding a packet. A template includes the type of the packet and all the
 * fields associated with the packet. 
 * @author Martin D. Flynn
 * @author Kiet Huynh
 *
 */
public class PayloadTemplate {

  // ------------------------------------------------------------------------

  /** The Primitive Mask value. */
  public static final int PRIMITIVE_MASK = 0x00F0;
  /** The Primitive Long value. */
  public static final int PRIMITIVE_LONG = 0x0010;
  /** The Primitive GPS value. */
  public static final int PRIMITIVE_GPS = 0x0030;
  /** The Primitive String value. */
  public static final int PRIMITIVE_STRING = 0x0040;
  /** The Primitive Pinary value. */
  public static final int PRIMITIVE_BINARY = 0x0050;

  // ------------------------------------------------------------------------

  /** Indicating Status Code field. */
  public static final int FIELD_STATUS_CODE = 0x01;
  
  /** Indicating 1st Timestamp field. */
  public static final int FIELD_TIMESTAMP_1 = 0x02;
  /** Indicating 2nd Timestamp field. */
  public static final int FIELD_TIMESTAMP_2 = 0x0D;
  
  /** Indicating Index field. */
  public static final int FIELD_INDEX = 0x03;
  /** Indicating Sequence field. */
  public static final int FIELD_SEQUENCE = 0x04;
  
  /** Indicating 1st GPS Point field. */
  public static final int FIELD_GPS_POINT_1 = 0x06;
  /** Indicating 2nd GPS Point field. */
  public static final int FIELD_GPS_POINT_2 = 0x0C;
 
  /** Indicating GPS Age field. */
  public static final int FIELD_GPS_AGE = 0x07; // %2u 0 to 65535 sec
  /** Indicating Speed field. */
  public static final int FIELD_SPEED = 0x08;
  /** Indicating Heading field. */
  public static final int FIELD_HEADING = 0x09;
  /** Indicating Altitude field. */
  public static final int FIELD_ALTITUDE = 0x0A;
  /** Indicating Distance field. */
  public static final int FIELD_DISTANCE = 0x0B;

  // Misc fields                                                 // Low                          High
  /** Indicating Geofence ID field. */
  public static final int FIELD_GEOFENCE_ID = 0x0E; // %4u 0x00000000 to 0xFFFFFFFF
  /** Indicating Top Speed field. */
  public static final int FIELD_TOP_SPEED = 0x0F; // %1u 0 to 255 kph             %2u 0.0 to 655.3 kph
  /** Indicating String field. */
  public static final int FIELD_STRING = 0x11; // %*s may contain only 'A'..'Z', 'a'..'z, '0'..'9', '-', '.'
  /** Indicating Binary field. */
  public static final int FIELD_BINARY = 0x1A; // %*b  

  // I/O fields                                                  // Low                          High
  /** Indicating Input ID field. */
  public static final int FIELD_INPUT_ID = 0x21; // %4u 0x00000000 to 0xFFFFFFFF
  /** Indicating Input state field. */
  public static final int FIELD_INPUT_STATE = 0x22; // %4u 0x00000000 to 0xFFFFFFFF
  /** Indicating Output ID field. */
  public static final int FIELD_OUTPUT_ID = 0x24; // %4u 0x00000000 to 0xFFFFFFFF
  /** Indicating Output State field. */
  public static final int FIELD_OUTPUT_STATE = 0x25; // %4u 0x00000000 to 0xFFFFFFFF
  /** Indicating Elapsed Time field. */
  public static final int FIELD_ELAPSED_TIME = 0x27; // %3u 0 to 16777216 sec        %4u 0.000 to 4294967.295 sec
  /** Indicating Counter field. */
  public static final int FIELD_COUNTER = 0x28; // %4u 0 to 4294967295
  /** Indicating Sensor32 Low field. */
  public static final int FIELD_SENSOR32_LOW = 0x31; // %4u 0x00000000 to 0xFFFFFFFF
  /** Indicating Sensor32 High field. */
  public static final int FIELD_SENSOR32_HIGH = 0x32; // %4u 0x00000000 to 0xFFFFFFFF
  /** Indicating Sensor32 Average field. */
  public static final int FIELD_SENSOR32_AVER = 0x33; // %4u 0x00000000 to 0xFFFFFFFF
  /** Indicating Temperature Low field. */
  public static final int FIELD_TEMP_LOW = 0x3A; // %1i -127 to +127 C           %2i -3276.7 to +3276.7 C
  /** Indicating Temperature High field. */
  public static final int FIELD_TEMP_HIGH = 0x3B; // %1i -127 to +127 C           %2i -3276.7 to +3276.7 C
  /** Indicating Temperature Average field. */
  public static final int FIELD_TEMP_AVER = 0x3C; // %1i -127 to +127 C           %2i -3276.7 to +3276.7 C

  // GPS quality fields                                          // Low                          High
  /** Indicating GPS DGPS Update field. */
  public static final int FIELD_GPS_DGPS_UPDATE = 0x41; // %2u 0 to 65535 sec
  /** Indicating GPS Horizontal Accuracy field. */
  public static final int FIELD_GPS_HORZ_ACCURACY = 0x42; // %1u 0 to 255 m               %2u 0.0 to 6553.5 m
  /** Indicating GPS Verticle Accuracy field. */
  public static final int FIELD_GPS_VERT_ACCURACY = 0x43; // %1u 0 to 255 m               %2u 0.0 to 6553.5 m
  /** Indicating GPS Satellites field. */
  public static final int FIELD_GPS_SATELLITES = 0x44; // %1u 0 to 12
  /** Indicating GPS Magnitude Variation field. */
  public static final int FIELD_GPS_MAG_VARIATION = 0x45; // %2i -180.00 to 180.00 deg
  /** Indicating GPS Qualigy field. */
  public static final int FIELD_GPS_QUALITY = 0x46; // %1u (0=None, 1=GPS, 2=DGPS, ...)
  /** Indicating GPS Type field. */
  public static final int FIELD_GPS_TYPE = 0x47; // %1u (1=None, 2=2D, 3=3D, ...)
  /** Indicating GPS Goefence ID Height field. */
  public static final int FIELD_GPS_GEOID_HEIGHT = 0x48; // %1i -128 to +127 m           %2i -3276.7 to +3276.7 m
  /** Indicating GPS PDOP field. */
  public static final int FIELD_GPS_PDOP = 0x49; // %1u 0.0 to 25.5              %2u 0.0 to 99.9
  /** Indicating GPS HDOP field. */
  public static final int FIELD_GPS_HDOP = 0x4A; // %1u 0.0 to 25.5              %2u 0.0 to 99.9
  /** Indicating GPS VDOP field. */
  public static final int FIELD_GPS_VDOP = 0x4B; // %1u 0.0 to 25.5              %2u 0.0 to 99.9

  // ------------------------------------------------------------------------

  private int customType = -1; // undefined
  private Field fields[] = null;
  private boolean repeatLast = false;

  /**
   * Creates a new PayloadTemplate instance. This constructor accepts 2
   * parameters, one for the type and of for the fields of the PayloadTemplate.
   * The "repeatLast" is initialized to false.
   * @param type The custom type 
   * @param flds An array of Field
   */
  public PayloadTemplate(int type, Field flds[]) {
    this.customType = type;
    this.fields = flds;
    this.repeatLast = false;
  }

  /**
   * Creates a new PayloadTemplate instance. This constructor accepts 3
   * parameters which will initialized all the instance variables of the 
   * PayloadTemplate.
   * @param type The custom type.
   * @param flds An array of Field.
   * @param repeatLast A boolean value.
   */
  public PayloadTemplate(int type, Field flds[], boolean repeatLast) {
    this.customType = type;
    this.fields = flds;
    this.repeatLast = repeatLast;
  }

  // ------------------------------------------------------------------------

  /**
   * Gets the type of the packet.
   * @return A number representing the type of the packet.
   */
  public int getPacketType() {
    return this.customType;
  }

  /**
   * Gets the Field at the specified index.
   * @param ndx The index of the desired field.
   * @return The Field at the specified index.
   */
  public Field getField(int ndx) {
    if ((ndx >= 0) && (this.fields != null) && (this.fields.length > 0)) {
      if (ndx < this.fields.length) {
        return this.fields[ndx];
      }
      else if (this.repeatLast) {
        return this.fields[this.fields.length - 1];
      }
    }
    return null;
  }

  /**
   * Returns "fields" of the PayloadTemplate.
   * @return A Field containing "fields" of the PayloadTemplate.
   */
  public Field[] getFields() {
    if (this.fields == null) {
      this.fields = new Field[0];
    }
    return this.fields;
  }

  /**
   * Returns the "repeatLast" of the PayloadTemplate.
   * @return The values of the "repeatLast".
   */
  public boolean getRepeatLast() {
    return this.repeatLast;
  }

  // ------------------------------------------------------------------------

  /**
   * Returns Payload for PKT_CLIENT_FORMAT_DEF_24 packet type.
   * @return The Payload.
   */
  public Payload getPayload() {
    Payload p = new Payload();
    Field fld[] = this.getFields();

    /* start with type and field count */
    p.writeULong(this.getPacketType(), 1);
    p.writeULong(fld.length, 1);

    /* write fields */
    for (int i = 0; i < fld.length; i++) {
      p.writeULong(fld[i].getMask(), 3);
    }

    /* return payload */
    return p;

  }

  // ------------------------------------------------------------------------
  // ------------------------------------------------------------------------
  // ------------------------------------------------------------------------

  private static final char FIELD_VALUE_SEPARATOR = '|';

  /**
   * Defines a single field of a packet. 
   * @author Martin D. Flynn
   * @author Kiet Huynh
   *
   */
  public static class Field {
    private boolean hiRes = false;
    private int fldType = -1;
    private int fldNdx = 0;
    private int fldLen = 0;

    /**
     * Creates a new Field instance. This constructor has 4 parameters that initialize
     * 4 instance variables.
     * @param type The type of the field.
     * @param hiRes The hiRes value.
     * @param index The index of the field.
     * @param length The length of the field.
     */
    public Field(int type, boolean hiRes, int index, int length) {
      this.fldType = type;
      this.hiRes = hiRes;
      this.fldNdx = index;
      this.fldLen = length;
    }
    
    /**
     * Creates a new Field instance. This constructor has only one parameter that
     * can initialize all 4 instance variables based on its hex value. The Mask layout
     * is:
     * <p>23:1  HiRes    (1 bit)     0x800000</p>
     * <p>16:7  Type     (0..63)     0x7F0000</p>
     * <p>8:8  Index    (0..15)     0x00FF00</p>
     * <p>0:8  Length   (0..15)     0x0000FF</p>
     * 
     * @param mask The number that will initialize all instance variables.
     */
    public Field(long mask) {
      // Mask Layout:
      //   23:1  HiRes    (1 bit)     0x800000
      //   16:7  Type     (0..63)     0x7F0000
      //    8:8  Index    (0..15)     0x00FF00
      //    0:8  Length   (0..15)     0x0000FF
      this.fldType = (int) (mask >> 16) & 0x7F;
      this.hiRes = ((mask & 0x800000) != 0);
      this.fldNdx = (int) (mask >> 8) & 0xFF;
      this.fldLen = (int) mask & 0xFF;
    }

    /**
     * Creates a new Field instance. This constructor has only one parameter of type
     * String. The string parameter will be parsed into string array with the indicated 
     * delimiter. The string array has the format:
     * <p> type|[H|L]|index|length </p>
     * The values in the array can be used to initialized all instance variables of this
     * Field.
     * @param s The input string.
     */
    public Field(String s) {
      // "<type>|[H|L]|<index>|<length>"
      String f[] = StringTools.parseString(s, FIELD_VALUE_SEPARATOR);
      this.fldType = (f.length > 0) ? (int) StringTools.parseLong(f[1], -1L) : -1;
      this.hiRes = (f.length > 1) ? f[0].equalsIgnoreCase("H") : false;
      this.fldNdx = (f.length > 2) ? (int) StringTools.parseLong(f[2], 0L) : 0;
      this.fldLen = (f.length > 3) ? (int) StringTools.parseLong(f[3], 0L) : 0;
    }

    /**
     * Gets the Mask of this Field.
     * @return The Mask.
     */
    public long getMask() {
      long mask = 0L;
      if (this.hiRes) {
        mask |= 0x800000;
      }
      mask |= (this.fldType << 16) & 0x7F0000;
      mask |= (this.fldNdx << 8) & 0x00FF00;
      mask |= (this.fldLen << 0) & 0x0000FF;
      return mask;
    }

    /**
     * Gets the types of the Field.
     * @return A number indicating the type the field.
     */
    public int getType() {
      return this.fldType;
    }

    /**
     * Gets the primitive type of the Field.
     * @return The primitive type.
     */
    public int getPrimitiveType() {
      switch (this.fldType) {
      case FIELD_GPS_POINT_1:
        return PRIMITIVE_GPS;
      case FIELD_GPS_POINT_2:
        return PRIMITIVE_GPS;
      case FIELD_STRING:
        return PRIMITIVE_STRING;
      case FIELD_BINARY:
        return PRIMITIVE_BINARY;
      default:
        return PRIMITIVE_LONG;
      }
    }

    /**
     * Returns True if the type is valid.
     * @return True if the type is valid.
     */
    public boolean isValidType() {
      return true;
    }

    /**
     * Returns true if the type is represented by a signed number.
     * @return True if the type is represented by a signed number.
     */
    public boolean isSigned() {
      switch (this.fldType) {
      case FIELD_GPS_MAG_VARIATION:
      case FIELD_GPS_GEOID_HEIGHT:
      case FIELD_ALTITUDE:
      case FIELD_TEMP_LOW:
      case FIELD_TEMP_HIGH:
      case FIELD_TEMP_AVER:
        return true;
      default:
        return false;
      }
    }

    /**
     * Returns true if the type is represented by a hexidecimal number.
     * @return TRUE if the type is represented by a hexidecimal number.
     */
    public boolean isHex() {
      switch (this.fldType) {
      case FIELD_HEADING:
        return !this.hiRes;
      case FIELD_STATUS_CODE:
      case FIELD_SEQUENCE:
      case FIELD_INPUT_ID:
      case FIELD_INPUT_STATE:
      case FIELD_OUTPUT_ID:
      case FIELD_OUTPUT_STATE:
      case FIELD_GEOFENCE_ID:
        return true;
      default:
        return false;
      }
    }
    /**
     * Returns True if this Field is HiRes.
     * @return True if this Field is HiRes.
     */
    public boolean isHiRes() {
      return this.hiRes;
    }

    /**
     * Returns the index of the Field.
     * @return The index of the Field.
     */
    public int getIndex() {
      return this.fldNdx;
    }

    /**
     * Returns the length of the Field.
     * @return The length of the Field.
     */
    public int getLength() {
      return this.fldLen;
    }

    /**
     * Parses a string into a payload.
     * @param s The string to be parsed.
     * @param sndx index of the string at where the value will be written to the payload. 
     * @param payload The payload into which to sring is parsed.
     * @return Not sure what will be returned.
     */
    public int parseString(String s[], int sndx, Payload payload) {
      // NOTE: This should specifically set the index to the proper payload location!!
      int length = this.getLength();
      switch (this.getPrimitiveType()) {
      case PRIMITIVE_GPS: {
        double lat = StringTools.parseDouble(s[sndx++], 0.0);
        double lon = (sndx < s.length) ? StringTools.parseDouble(s[sndx++], 0.0) : 0.0;
        payload.writeGPS(new GeoPoint(lat, lon), length);
        break;
      }
      case PRIMITIVE_STRING: {
        payload.writeString(s[sndx++], length);
        break;
      }
      case PRIMITIVE_BINARY: {
        byte b[] = StringTools.parseHex(s[sndx++], new byte[0]);
        payload.writeBytes(b, length);
        break;
      }
      case PRIMITIVE_LONG:
      default: {
        long val = s[sndx].startsWith("0x") ? StringTools.parseHexLong(s[sndx++], 0L) : StringTools
            .parseLong(s[sndx++], 0L);
        if (this.isSigned()) {
          payload.writeLong(val, length);
        }
        else {
          payload.writeULong(val, length);
        }
        break;
      }
      }
      return sndx;
    }

    /**
     * Represents this Field as a string.
     * @return The string representing this Field.
     */
    public String toString() {
      StringBuffer sb = new StringBuffer();
      sb.append(this.getType());
      sb.append(FIELD_VALUE_SEPARATOR);
      sb.append(this.isHiRes() ? "H" : "L");
      sb.append(FIELD_VALUE_SEPARATOR);
      sb.append(this.getIndex());
      sb.append(FIELD_VALUE_SEPARATOR);
      sb.append(this.getLength());
      return sb.toString();
    }

    /**
     * Returns True if 2 Field objects have the same values.
     * @param other The object to be compared with this PayloadTemplate object.
     * @return True if 2 Field objects have the same values.
     */
    public boolean equals(Object other) {
      if (other instanceof Field) {
        return this.toString().equals(other.toString());
      }
      else {
        return false;
      }
    }

  }

}
